﻿// <copyright file="WpfHitTestService.cs" company="Ucaya">
// </copyright>
// <author>Ucaya</author>

namespace KinectExtensions.Services
{
    #region

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Windows;
    using System.Windows.Input;
    using System.Windows.Media;

    using KinectExtensions.Helpers;
    using KinectExtensions.Nui;

    #endregion

    /// <summary>
    ///   TODO: Update summary.
    /// </summary>
    [Export(typeof(IHitTestService))]
    public class WpfHitTestService : IHitTestService
    {
        #region Constants and Fields

        /// <summary>
        ///   The cache.
        /// </summary>
        private readonly List<Tuple<WeakReference, HitResult>> cache = new List<Tuple<WeakReference, HitResult>>();

        #endregion

        #region Constructors and Destructors

        /// <summary>
        ///   Initializes a new instance of the <see cref = "WpfHitTestService" /> class.
        /// </summary>
        public WpfHitTestService()
        {
            ServiceLocator.Instance.SatisfyImportsOnce(this);
            this.HandTrackingService.HandPositionsChanged += this.HandTrackingService_HandPositionsChanged;
            InputManager.Current.HitTestInvalidatedAsync += this.Current_HitTestInvalidatedAsync;
        }

        #endregion

        #region Public Events

        /// <summary>
        ///   The hit may have changed.
        /// </summary>
        public event EventHandler HitMayHaveChanged;

        #endregion

        #region Public Properties

        /// <summary>
        ///   Gets or sets CursorService.
        /// </summary>
        [Import]
        public CursorService CursorService { get; set; }

        /// <summary>
        ///   Gets or sets HandTrackingService.
        /// </summary>
        [Import]
        public HandTrackingService HandTrackingService { get; set; }

        #endregion

        #region Public Methods

        /// <summary>
        ///   The hit test hand.
        /// </summary>
        /// <param name = "source">
        ///   The source.
        /// </param>
        /// <param name = "handPosition">
        ///   The hand position.
        /// </param>
        /// <returns>
        /// </returns>
        public HitResult HitTestHand(FrameworkElement source, HandPosition handPosition)
        {
            if (source != null && handPosition != null && this.CursorService != null && this.CursorService.CursorProvider != null)
            {
                long lastFrameTimeStamp = handPosition.TimeStamp;
                this.cache.RemoveAll(_ => _.Item1.Target == null);
                Tuple<WeakReference, HitResult> cacheValue = this.cache.Find(_ => _.Item1.Target == handPosition);
                if (cacheValue == null)
                {
                    cacheValue = Tuple.Create(new WeakReference(handPosition), HitResult.None);
                    this.cache.Add(cacheValue);
                }
                else
                {
                    if (cacheValue.Item2.TimeStamp == lastFrameTimeStamp)
                    {
                        return cacheValue.Item2;
                    }
                }

                Window root = Application.Current.MainWindow;
                if (root == null)
                {
                    return HitResult.None;
                }

                ICursor visual = this.CursorService.CursorProvider.GetCursor(handPosition);
                if (visual == null || !source.IsLoaded)
                {
                    return HitResult.None;
                }

                Rect bounds;
                GeneralTransform gt;
                Rect localBounds;
                try
                {
                    bounds = visual.GetBounds();
                    bounds.Offset(visual.Location.X, visual.Location.Y);
                    gt = source.TransformToVisual(root).Inverse;
                    localBounds = gt.TransformBounds(bounds);
                }
                catch
                {
                    return HitResult.None;
                }

                Rect rect = CustomVisualTreeHelper.GetHitableDescendantBounds(source);
                bool isSuccess = localBounds.IntersectsWith(rect);

                var hitResult = new HitResult();

                if (isSuccess)
                {
                    Point localPoint = gt.Transform(visual.Location);
                    localPoint.Offset(-rect.X, -rect.Y);
                    hitResult.RelativeTouchPosition = new Point(localPoint.X / rect.Width, localPoint.Y / rect.Height);
                }

                hitResult.TimeStamp = lastFrameTimeStamp;
                hitResult.Result = isSuccess;
                hitResult.CurrentKinectZone = handPosition.KinectZone;
                return hitResult;
            }

            return HitResult.None;
        }

        /// <summary>
        ///   The hit test hand master.
        /// </summary>
        /// <param name = "source">
        ///   The source.
        /// </param>
        /// <returns>
        /// </returns>
        public HitResult HitTestHandMaster(FrameworkElement source)
        {
            if (this.HandTrackingService == null)
            {
                return HitResult.None;
            }

            return this.HitTestHand(source, this.HandTrackingService.HandMaster);
        }

        #endregion

        #region Methods

        /// <summary>
        ///   The current_ hit test invalidated async.
        /// </summary>
        /// <param name = "sender">
        ///   The sender.
        /// </param>
        /// <param name = "e">
        ///   The e.
        /// </param>
        private void Current_HitTestInvalidatedAsync(object sender, EventArgs e)
        {
            this.OnHitTestMayHaveChanged();
        }

        /// <summary>
        ///   The hand tracking service_ hand positions changed.
        /// </summary>
        /// <param name = "sender">
        ///   The sender.
        /// </param>
        /// <param name = "e">
        ///   The e.
        /// </param>
        private void HandTrackingService_HandPositionsChanged(object sender, HandPositionChangedEventArgs e)
        {
            this.OnHitTestMayHaveChanged();
        }

        /// <summary>
        ///   The on hit test may have changed.
        /// </summary>
        private void OnHitTestMayHaveChanged()
        {
            if (this.HitMayHaveChanged != null)
            {
                this.HitMayHaveChanged(this, EventArgs.Empty);
            }
        }

        #endregion
    }
}